const fs = require('fs');
const async = require('async');
const request = require('request');
const Keyv = require('keyv');
const KeyvFile = require('keyv-file');
const dJSON = require('dirty-json');
const {registerFont, createCanvas} = require('canvas');
const legacy = require('legacy-encoding');
const Discord = require('discord.js');
const irc = require('irc-upd');

registerFont('pxplus_ibm_vga8.ttf', {family: 'nfofont'});

// Load external data
const patterns = dJSON.parse(fs.readFileSync('pre-patterns.json', 'utf8'));
const nopeList = dJSON.parse(fs.readFileSync('pre-blacklist.json', 'utf8'));
const settings = dJSON.parse(fs.readFileSync('pre-settings.json', 'utf8'));
const strings = dJSON.parse(fs.readFileSync('pre-strings.json', 'utf8'));
const channels = settings.channels;
const dToken = settings.discordToken;
const ircNick = settings.ircNick;
let tags = {};

const keyv = new Keyv({
  store: new KeyvFile({
    encode: JSON.stringify,
    decode: JSON.parse,
    filename: 'pre-storage.json'
  })
});

const addTag = (tag, channelId) => {
  if (tags[tag] === undefined) {
    tags[tag] = [channelId];
  } else if (tag !== null && tag !== '') {
    tags[tag].push(channelId);
  }
};

const removeTag = (tag, channelId) => {
  if (tags[tag] !== undefined && tag !== null && tag !== '') {
    tags[tag].pop(channelId);
    if (tags[tag].length === 0) {
      delete tags[tag];
    }
  }
};

const srrDB = (rlsName, callback) => {
  request.get(`https://www.srrdb.com/api/details/${rlsName}`)
    .on('response', (response) => {
      var data = [];
      response.on('data', (chunk) => {
        data.push(chunk);
      });
      response.on('end', () => {
        let srrdbResponse = data.join('');
        if (srrdbResponse === '') {
          callback(new Error('Not available on SRRDB yet.'));
        } else {
          try {
            let output = {'size': null, 'archive_count': null, 'files': null};
            let srrDBData = JSON.parse(srrdbResponse);

            if (srrDBData.files) {
              // filter out sfv/nfo
              let files = srrDBData.files.filter((file) => {
                return (!(file.name.endsWith('.sfv') || file.name.endsWith('.nfo')));
              });
              let size = 0;
              files.forEach((file) => {
                size = size + parseInt(file.size);
              });

              output.size = size;
              output.archive_count = files.length;
            }

            if (srrDBData['archived-files'].length > 0) {
              output.files = srrDBData['archived-files'].filter((file) => {
                return file.size !== 0;
              });
            }
            callback(null, output);
          } catch (e) {
            callback(new Error('Failed to parse JSON'));
          }
        }
      });
    })
    .on('error', (err) => {
      callback(err);
    });
};

/*
 Gets NFO from SRRDB
 Returns URL to NFO
 */
const srrDBNfo = (rlsName) => {
  return new Promise((resolve, reject) => {
    request.get(`https://www.srrdb.com/api/nfo/${rlsName}`)
      .on('response', (response) => {
        var data = [];
        response.on('data', (chunk) => {
          data.push(chunk);
        });
        response.on('end', () => {
          let srrdbResponse = JSON.parse(data.join(''));
          if (srrdbResponse.nfolink.length === 0) {
            throw new Error('Not available on SRRDB yet.');
          } else {
            resolve(srrdbResponse.nfolink[0]);
          }
        });
      })
      .on('error', (err) => {
        resolve(err);
      });
  });
};

const uploadImgbb = (data) => {
  return new Promise((resolve, reject) => {
    request('https://imgbb.com/', (err, response, body) => {
      var authkey = /PF\.obj\.config\.auth_token="([a-z0-9]+)";/.exec(body)[1];
      request.post({
        url: 'https://imgbb.com/json',
        headers: {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0'
        },
        formData: {
          type: 'file',
          action: 'upload',
          auth_token: authkey,
          source: {
            value: data,
            options: {
              filename: 'nfo.png'
            }
          }
        }
      }, (err, httpResp, res) => {
        var ret = JSON.parse(res);
        resolve(ret.image.url);
      });
    });
  });
};


/* TODO: port from old code, clean this up */
const createNFO = (url) => {
  return new Promise((resolve, reject) => {
    request.get(url, {encoding: null}, async (err, response, body) => {
      let nfo = legacy.decode(body, 'cp437');
      nfo = nfo.replace(/[^\S\r\n]+$/gm, '').replace(/<br \/>[\s\S]+?failed announce/g, ''); // Strip empty spaces at the end
      nfo = nfo.split(/\r?\n/);
      var length = nfo.slice().sort((a, b) => {
        return b.length - a.length;
      })[0].length;

      let fontsize = 16;
      let imageHeight = Math.ceil(nfo.length * fontsize);
      let imageWidth = Math.ceil(length / 2 * fontsize);

      let canvas = createCanvas(imageWidth, imageHeight);
      let ctx = canvas.getContext('2d');
      ctx.fillStyle = '#33363c';
      ctx.fillRect(0, 0, imageWidth, imageHeight);

      ctx.font = fontsize + 'px nfofont';
      ctx.fillStyle = '#fff';
      var currentLineHeight = fontsize;
      for (var i = 0; i < nfo.length; i++) {
        ctx.fillText(nfo[i], 0, currentLineHeight);
        currentLineHeight += fontsize;
      }

      let imagedataURL = canvas.toDataURL();
      let imageBuffer = Buffer.from(imagedataURL.replace('data:image/png;base64,', ''), 'base64');
      let imageUrl = await uploadImgbb(imageBuffer);
      resolve(imageUrl);
    });
  });
};

/*
 Add extra info to RichEmbed
 Takes release details then edits prev rls message with it
 
 editType: nfo|details|both
 */
const editMessage = async (rlsName, editType, data) => {
  // Generate more data for embeds
  let nfoUrl = null;
  let archiveCount = null;
  let size = null;
  let filesStr = null;

  if (editType === 'nfo' || editType === 'both') {
    // Get nfo from srrDB
    await srrDBNfo(rlsName)
      .then(async (nfoLink) => {
        // build and upload NFO image
        nfoUrl = await createNFO(nfoLink);
      });
  }

  if (editType === 'details' || editType === 'both') {
    if (data.archive_count) {
      archiveCount = data.archive_count;
    }

    if (data.size) {
      size = formatBytes(data.size);
    }

    if (data.files) {
      let filesOutput = [];
      data.files.forEach((file) => {
        filesOutput.push(`**${file.name}** (${formatBytes(file.size)} | CRC ${file.crc})`);
      });
      filesStr = filesOutput.join('\n\n');
    }
  }

  // Send to channels
  let storedChannels = await keyv.get(rlsName);
  for (const channelId of Object.keys(storedChannels)) {
    const messages = storedChannels[channelId];
    let channel = dClient.channels.get(channelId);
    messages.forEach((messageId) => {
      channel.fetchMessage(messageId)
        .then(async (msg) => {
          // Add generated data to old embeds
          let preEmbed = new Discord.RichEmbed(msg.embeds[0]); // create new RichEmbed from recieved embed
          if (nfoUrl) {
            preEmbed.setImage(nfoUrl);
          }

          if (archiveCount) {
            preEmbed.addField('Archives', archiveCount, true);
          }

          if (size) {
            preEmbed.addField('Size', size, true);
          }

          if (filesStr) {
            preEmbed.addField('Files', filesStr);
          }

          msg.edit(preEmbed);
          console.info(`Added info for ${rlsName} to ${channel}`);
        });
    });
  }
};

/*
 Escape Discord-style markdown
 https://stackoverflow.com/a/39543625
 */
const escapeMarkdown = (str) => {
  var unescaped = str.replace(/\\(\*|_|`|~|\\)/g, '$1'); // unescape any "backslashed" character
  var escaped = unescaped.replace(/(\*|_|`|~|\\)/g, '\\$1'); // escape *, _, `, ~, \
  return escaped;
};

/*
 Convert bytes to a human readable format
 https://stackoverflow.com/a/18650828
 */
const formatBytes = (bytes, decimals = 2) => {
  if (bytes === 0)
    return '0 Bytes';

  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};

/*
 Parse RegExp string to object (basically just move the flag over)
 */
const parseRegexpStr = (str) => {
  let getFlag = str.match(/\/[igmsuy]+$/);
  var flags = '';

  // Check flag
  if (getFlag !== null && getFlag[0][0] === '/') {
    str = str.replace(getFlag[0], '');
    flags = getFlag[0].slice(1);
  }

  if (str[0] === '/') {
    str = str.slice(1);
  }

  if (str[str.length - 1] === '/') {
    str = str.slice(0, -1);
  }
  return new RegExp(str, flags);
};

// Configure clients
const dClient = new Discord.Client();
const iClient = new irc.Client('irc.opentrackers.org', ircNick, {
  channels: ['#pre', '#pre-info'],
  stripColors: true,
  debug: false,
  autoConnect: false
});

// Discord events
dClient.on('ready', async () => {
  console.log(`Connected to Discord as ${dClient.user.tag}!`);
  iClient.connect();

  // Get channels with tags
  let usersWithTags = await keyv.get('users_with_tags');

  // Add user tags to tags array
  if (usersWithTags !== undefined) {
    usersWithTags.forEach(async (channelId) => {
      let userTags = await keyv.get(`tags_${channelId}`);
      userTags.forEach((tag) => {
        addTag(tag, channelId);
      });
    });
  }
});

// catch so it doesn't crash
dClient.on('error', (error) => {
  //console.error('Discord error:', error);
});

dClient.on('message', async (msg) => {
  // If bot was mentioned
  if (msg.isMentioned(dClient.user.id)) {
    if (msg.content.includes('setgame=') && settings['admins'].includes(msg.author.id)) {
      dClient.user.setActivity(msg.content.match(/setgame=(.*)/)[1]);
    } else if (msg.content.includes('testpre=') && settings['admins'].includes(msg.author.id)) {
      // Send test alert to all channels
      async.each(channels, (channelId, callback) => {
        let channel = dClient.channels.get(channelId);
        let preEmbed = new Discord.RichEmbed();
        preEmbed.setColor(16141131).setTitle('⚠ PRE TEST ⚠');
        channel.send({embed: preEmbed}).then(() => {
          callback();
        });
      }, (err) => {
        // Send PRE after all test alerts sent
        if (err) {
          throw err;
        } else {
          preListen('yo mamma', `[PRE] [GAME] ${msg.content.match(/testpre=(.*)/)[1]}`);
        }
      });
    } else if (msg.content.toLowerCase().includes('good') && msg.content.toLowerCase().includes('bad') && msg.content.toLowerCase().includes('bot')) {
      // Be a confused bot
      let preEmbed = new Discord.RichEmbed();
      preEmbed.setColor(16141131).setTitle('Make up your mind dude');
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' confused the bot.');
    } else if ((msg.content.toLowerCase().includes('good') || msg.content.toLowerCase().includes('great')) && msg.content.toLowerCase().includes('bot')) {
      // Be a good bot
      let preEmbed = new Discord.RichEmbed();
      let response = strings['goodBotMsg'][(Math.floor(Math.random() * strings['goodBotMsg'].length))];
      preEmbed.setColor(16141131).setTitle(response);
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' called the bot good :D .');
    } else if ((msg.content.toLowerCase().includes('bad') || msg.content.toLowerCase().includes('shit') || msg.content.toLowerCase().includes('garbage')) && msg.content.toLowerCase().includes('bot')) {
      // Be a bad bot
      let preEmbed = new Discord.RichEmbed();
      let response = strings['badBotMsg'][(Math.floor(Math.random() * strings['badBotMsg'].length))];
      preEmbed.setColor(16141131).setTitle(response);
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' called the bot bad >:( .');
    } else if (msg.content.toLowerCase().includes('joke')) {
      // Be a funny bot
      let preEmbed = new Discord.RichEmbed();
      let response = strings['jokes'][(Math.floor(Math.random() * strings['jokes'].length))];
      preEmbed.setColor(16141131).setTitle(response);
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' asked for a joke.');
    }
  } else if (msg.guild === null) {
    // If message was dm

    if (msg.content.toLowerCase().includes('good') && msg.content.toLowerCase().includes('bad') && msg.content.toLowerCase().includes('bot')) {
      // Be a confused bot
      let preEmbed = new Discord.RichEmbed();
      preEmbed.setColor(16141131).setTitle('Make up your mind dude');
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' confused the bot.');
    } else if ((msg.content.toLowerCase().includes('good') || msg.content.toLowerCase().includes('great')) && msg.content.toLowerCase().includes('bot')) {
      // Be a good bot
      let preEmbed = new Discord.RichEmbed();
      let response = strings['goodBotMsg'][(Math.floor(Math.random() * strings['goodBotMsg'].length))];
      preEmbed.setColor(16141131).setTitle(response);
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' called the bot good :D .');
    } else if ((msg.content.toLowerCase().includes('bad') || msg.content.toLowerCase().includes('shit') || msg.content.toLowerCase().includes('garbage')) && msg.content.toLowerCase().includes('bot')) {
      // Be a bad bot
      let preEmbed = new Discord.RichEmbed();
      let response = strings['badBotMsg'][(Math.floor(Math.random() * strings['badBotMsg'].length))];
      preEmbed.setColor(16141131).setTitle(response);
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' called the bot bad >:( .');
    } else if (msg.content.toLowerCase().match(/^add ([A-z\d._-]+)$/)) {

      // Get user tags
      let existingTags = await keyv.get(`tags_${msg.channel.id}`);

      // Get given tag
      let tag = Discord.escapeMarkdown(msg.content.toLowerCase().split('add ')[1]);
      let preEmbed = new Discord.RichEmbed();
      if (existingTags && existingTags.indexOf(tag) > -1) {
        // If tag already exists
        preEmbed.setColor(16141131).setTitle('Tag already exists.');
      } else if (existingTags && existingTags.length >= 25) {
        // If reached tag limit
        preEmbed.setColor(16141131).setTitle('You reached the maximum tag limit of 25. You cannot add any more tags. You can remove an existing tag if you want to add a new one.');
      } else if (tag !== undefined && tag !== null) {

        // Add tag to memory
        addTag(tag, msg.channel.id);

        // Add tag
        if (existingTags !== undefined) {
          await keyv.set(`tags_${msg.channel.id}`, existingTags.concat(tag));
          let tagsString = '';
          existingTags.forEach((tag) => {
            tagsString += tag + '\n';
          });
          preEmbed.setColor(16141131).setTitle('Added tag: "' + tag + '"');
          preEmbed.setColor(16141131).setDescription('Tags: \n' + tagsString + tag);
          console.log(msg.author.username + ' added "' + tag + '" to their tags.');
        } else {
          await keyv.set(`tags_${msg.channel.id}`, [tag]);
          // Add channel to users_with_tags array
          let usersWithTags = await keyv.get('users_with_tags');
          if (usersWithTags !== undefined) {
            await keyv.set('users_with_tags', usersWithTags.concat(msg.channel.id));
          } else {
            await keyv.set('users_with_tags', [msg.channel.id]);
          }
          preEmbed.setColor(16141131).setTitle('Added tag: "' + tag + '"');
          preEmbed.setColor(16141131).setDescription('Tags: \n' + tag);
          console.log(msg.author.username + ' added "' + tag + '" to their tags.');
        }
      } else {
        preEmbed.setColor(16141131).setTitle('Unable to add tag.');
      }
      msg.channel.send(preEmbed);
    } else if (msg.content.toLowerCase().match(/^remove ([A-z\d._-]+)$/)) {

      // Get user tags
      let existingTags = await keyv.get(`tags_${msg.channel.id}`);

      // Get given tag
      let tag = Discord.escapeMarkdown(msg.content.toLowerCase().split('remove ')[1]);
      let preEmbed = new Discord.RichEmbed();

      // Remove tag from memory
      removeTag(tag, msg.channel.id);
      // Remove tag
      if (existingTags.indexOf(tag) > -1) {
        existingTags.splice(existingTags.indexOf(tag), 1);
        await keyv.set(`tags_${msg.channel.id}`, existingTags);
        let tagsString = '';
        existingTags.forEach((tag) => {
          tagsString += tag + '\n';
        });
        preEmbed.setColor(16141131).setTitle('Removed tag: "' + tag + '"');
        preEmbed.setColor(16141131).setDescription('Tags: \n' + tagsString);
        console.log(msg.author.username + ' removed "' + tag + '" from their tags.');
      } else {
        preEmbed.setColor(16141131).setTitle('Given tag does not exist.');
      }
      msg.channel.send(preEmbed);

      // If empty, remove from channels with tags
      if (tags[tag] && tags[tag].length === 0) {
        let usersWithTags = await keyv.get('users_with_tags');
        if (usersWithTags) {
          usersWithTags.pop(msg.channel.id);
          await keyv.set('users_with_tags', usersWithTags);
        }
      }
    } else if (msg.content.toLowerCase() === 'show tags') {
      // Show user tags
      let existingTags = await keyv.get(`tags_${msg.channel.id}`);
      let preEmbed = new Discord.RichEmbed();
      if (existingTags !== undefined && existingTags.length > 0) {
        let tagsString = '';
        existingTags.forEach((tag) => {
          tagsString += tag + '\n';
        });
        preEmbed.setColor(16141131).setTitle('Tags');
        preEmbed.setColor(16141131).setDescription(tagsString);
      } else {
        preEmbed.setColor(16141131).setTitle('No tags have been added yet.');
      }
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' requested to see their tags.');
    } else if (msg.content.toLowerCase() === 'clear') {
      // Clear user tags
      let usersWithTags = await keyv.get('users_with_tags');
      if (usersWithTags) {
        usersWithTags.pop(msg.channel.id);
        await keyv.set('users_with_tags', usersWithTags);
      }
      let existingTags = await keyv.get(`tags_${msg.channel.id}`);
      existingTags.forEach((tag) => {
        removeTag(tag, msg.channel.id);
      });
      await keyv.delete(`tags_${msg.channel.id}`);
      let preEmbed = new Discord.RichEmbed();
      preEmbed.setColor(16141131).setTitle('All tags have been deleted');
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' cleared their tags.');
    } else if (msg.content.toLowerCase() === 'joke') {
      // Be a funny bot
      let preEmbed = new Discord.RichEmbed();
      let response = strings['jokes'][(Math.floor(Math.random() * strings['jokes'].length))];
      preEmbed.setColor(16141131).setTitle(response);
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' asked for a joke.');
    } else if (msg.content.toLowerCase() === 'help') {
      // Be a funny bot
      let preEmbed = new Discord.RichEmbed();
      preEmbed.setColor(16141131).setDescription(strings['help']);
      preEmbed.setColor(16141131).setTitle('Help');
      msg.channel.send(preEmbed);
      console.log(msg.author.username + ' asked for help.');
    }
  }
});

dClient.login(dToken);

// IRC events
const preListen = (from, msg) => {
  var msgStripped = msg.replace(/\[PRE\] \[.*\] /, '');
  var msgRaw = msg;
  var matched = patterns.some(exp => parseRegexpStr(exp).test(msgRaw));
  if (matched) {
    console.info(`Matched: ${msgRaw}`);

    var blacklisted = nopeList.some(str => msgStripped.includes(str));
    if (blacklisted) {
      console.info('Release matched blacklist');
      return;
    }

    // Create empty embed
    var preEmbed = new Discord.RichEmbed();

    var isNukeMsg = (/\[(?:MOD|UN)?NUKE\]/.test(msgRaw));
    if (isNukeMsg) {
      console.info('Found nuke message');
      if (msg.includes('[NUKE]') || msg.includes('[MODNUKE]')) {
        preEmbed.setColor(13175306).setTitle(escapeMarkdown(msgRaw));
      } else if (msg.includes('[UNNUKE]')) {
        preEmbed.setColor(7984640).setTitle(escapeMarkdown(msgRaw));
      }
    } else {
      preEmbed.setColor(7054335).setTitle(escapeMarkdown(msgStripped));
      preEmbed.setDescription(`[srrDB](https://www.srrdb.com/release/details/${msgStripped}) | [PreDB.me](http://predb.me/?search=${msgStripped})`);

      // Call srrDB() at max 60 times waiting 5 seconds every retry
      async.retry({times: 60, interval: 5 * 1000}, (callback) => {
        return srrDB(msgStripped, callback);
      }, (err, result) => {
        if (err) {
          console.info(`Gave up adding NFO for ${msgStripped}`);
        } else {
          // Add details & nfo
          editMessage(msgStripped, 'both', result);
        }
      });
    }

    // Get all channels that should receive the message
    let receivingChannels = channels.slice(0);
    Object.keys(tags).forEach((tagKey) => {
      if (msgRaw.toString().toLowerCase().includes(tagKey)) {
        tags[tagKey].forEach((channelId) => {
          receivingChannels.push(channelId);
        });
      }
    });
    // Remove duplicates
    receivingChannels = [...new Set(receivingChannels)];

    // Send to all
    receivingChannels.forEach((channelId) => {
      let channel = dClient.channels.get(channelId);
      channel.send({embed: preEmbed})
        .then((msg) => {
          // after message sent
          if (msgRaw.includes('SKIDROW')) {
            // add shit/kids row
            let shit = ['💩', '👶'];
            let rand = shit[Math.floor(Math.random() * shit.length)];
            msg.react(rand)
              .then((msg) => {
                msg.message.react('🚣');
              });
          }
          return msg;
        })
        .then(async (msg) => {
          // Store message info in this format
          /*
           {
           "channel_id": ["message_id", "message_id2"]
           }
           */
          var stored = await keyv.get(msgStripped);
          if (typeof (stored) !== 'undefined') {
            if (!(msg.channel.id in stored)) {
              // make new empty channel key if doesn't exist yet
              stored[msg.channel.id] = [msg.id];
            } else {
              // channel key already exists so just add the new message id
              stored[msg.channel.id].push(msg.id);
            }
            await keyv.set(msgStripped, stored, 30 * 60 * 1000); // expire in 30 minutes
          } else {
            // Rls not stored yet so store a new object
            var struct = {};
            struct[msg.channel.id] = [msg.id];
            await keyv.set(msgStripped, struct, 30 * 60 * 1000); // expire in 30 minutes
          }
        });
    });
  }
};

iClient.on('registered', () => {
  console.info(`Connected to IRC as ${ircNick}`);
});

iClient.addListener('message#pre', preListen);
iClient.addListener('pm', preListen);
